package com.planet.processor


import android.webkit.JavascriptInterface
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.*
import java.text.SimpleDateFormat
import java.util.*
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.*
import javax.lang.model.util.Elements
import javax.tools.Diagnostic
import kotlin.collections.ArrayList

@AutoService(Processor::class)
@SupportedOptions(JsModuleMergeProcessor.KAPT_KOTLIN_GENERATED_OPTION_NAME)
class JsModuleMergeProcessor : AbstractProcessor() {


    val KDOC = "根据所有被 JsModule 注解过的类中的 带 JavascriptInterface 的方法全部合并到一起;" +
            "暂时所有的 module 都必须添加进去，后期可空"

    val CLASS_SIMPLE_NAME = "JsInjector"

    companion object {
        const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "merger.kapt.kotlin.generated"
    }

    override fun getSupportedAnnotationTypes(): MutableSet<String> {
        return mutableSetOf(JsModule::class.java.name)
    }

    override fun getSupportedSourceVersion(): SourceVersion {
        return SourceVersion.latest()
    }

    private var elementUtils: Elements? = null
    private var messager: Messager? = null
    private var mFiler: Filer? = null


    @Synchronized
    override fun init(processingEnv: ProcessingEnvironment) {
        super.init(processingEnv)
        mFiler = processingEnv.filer
        elementUtils = processingEnv.elementUtils
        messager = processingEnv.messager

        messager?.printMessage(
                Diagnostic.Kind.NOTE,
                "env:$processingEnv  elementUtils:$elementUtils  messager:$messager"
        )

    }

    override fun process(
            annotations: MutableSet<out TypeElement>?,
            roundEnv: RoundEnvironment?
    ): Boolean {

        val elements = roundEnv?.getElementsAnnotatedWith(JsModule::class.java)

        if (elements?.isEmpty() != false) return false
        messager?.printMessage(Diagnostic.Kind.NOTE, "elements------>: ${elements?.toList()}")

        if (elements.isEmpty())
            return false
        var pkgName: String? = null

        val brewClassInfoList = arrayListOf<ClassInfo>()

        elements?.forEach { classElement ->
            if (pkgName == null)
                pkgName = getPackageName(classElement)
            val classInfo = findClassInfo(classElement)
            brewClassInfoList.add(classInfo)
        }



        if (pkgName != null)
            brewJsClass(pkgName!!, brewClassInfoList)

        return true
    }

    private fun findClassInfo(classElement: Element): ClassInfo {

        val pkgName = getPackageName(classElement)

        val className = (classElement as QualifiedNameable).simpleName.toString()

        val methods = arrayListOf<MethodInfo>()

        classElement.enclosedElements.forEach {

            val methodInfo = getMethodInfo(it)
            if (methodInfo != null)
                methods.add(methodInfo)
        }


        return ClassInfo(
                pkgName!!,
                className,
                classElement.asType().asTypeName(),
                methods
        )

    }


    private fun getPackageName(ele: Element?): String? {
        if (ele == null) return null

        if (ele is TypeElement) {
            val packageElement = elementUtils?.getPackageOf(ele)
            if (!packageElement?.isUnnamed!!) {
                return packageElement.qualifiedName.toString()
            }
        }
        return null
    }

    private fun getMethodInfo(methodElement: Element): MethodInfo? {

        if (methodElement.getAnnotation(JavascriptInterface::class.java) != null) {
            if (methodElement.kind == ElementKind.METHOD) {
                val exeElement = methodElement as ExecutableElement

                val name = exeElement.simpleName.toString()

                val returnTypeName = exeElement.returnType.asTypeName()

                val methodParameters = exeElement.parameters

                val parameters = arrayListOf<VariableElement>()

                methodParameters.forEach {
                    messager?.printMessage(Diagnostic.Kind.NOTE, "args:${it.simpleName}")
                    parameters.add(it)
                }

                return MethodInfo(name, returnTypeName = returnTypeName, parameters = parameters)
            }
        }

        return null
    }


    private fun brewJsClass(pkgName: String, classInfoList: List<ClassInfo>) {

        //class declaration

        val typeSpec = TypeSpec.classBuilder(ClassName(pkgName, CLASS_SIMPLE_NAME))


        createConstructor(typeSpec, classInfoList)

        //module field declaration

        val modulePropertyNames = addProperties(typeSpec, classInfoList)

        //method generation

        createFunctions(typeSpec, modulePropertyNames, classInfoList)


        createBuilderClass(pkgName, typeSpec, modulePropertyNames, classInfoList)

        //create  构建方法生成


        //injection

        FileSpec.builder(pkgName, CLASS_SIMPLE_NAME)
                .addType(typeSpec.build())
                .addComment("$KDOC\n generated time:${SimpleDateFormat.getDateTimeInstance().format(Date())}")
                .build()
                .writeTo(mFiler!!)

    }

    private fun createConstructor(
            typeSpec: TypeSpec.Builder,
            classInfoList: List<ClassInfo>
    ) {


        val primaryConstructorSpec = FunSpec.constructorBuilder()


        classInfoList.forEach {

            primaryConstructorSpec.addParameter(
                    ParameterSpec.builder(it.className.toLowerCase(Locale.CHINA) + "Instance", it.classTypeName)
                            .build()
            )

            typeSpec.addProperty(
                    PropertySpec.builder(it.className.toLowerCase(Locale.CHINA) + "Instance", it.classTypeName)
                            .initializer(it.className.toLowerCase(Locale.CHINA) + "Instance")
                            .mutable(true)
                            .build()
            )
        }


        typeSpec.primaryConstructor(primaryConstructorSpec.build())

    }

    private fun createBuilderClass(
            pkgName: String,
            typeSpec: TypeSpec.Builder,
            modulePropertyNames: java.util.ArrayList<String>,
            classInfoList: List<ClassInfo>
    ) {


        //builder
        val builderSpec = TypeSpec.classBuilder("Builder")


        val factoryClassName = ClassName(pkgName, CLASS_SIMPLE_NAME)
        val builderType = ClassName(pkgName, "Builder")


        //add properties
        val properties = arrayListOf<String>()
        classInfoList.forEach {

            val moduleInstanceName = it.className.toLowerCase(Locale.CHINA) + "Instance"

            properties.add(moduleInstanceName)
            val propertySpec = PropertySpec.builder(
                    moduleInstanceName,
                    it.classTypeName,
                    KModifier.LATEINIT
            ).mutable(true).build()

            builderSpec.addProperty(propertySpec)


            //adModule

            val addModuleFuncSpec = FunSpec.builder("add${it.className}")
                    .addParameter(ParameterSpec(it.className.camelCase(), it.classTypeName))
                    .addCode(CodeBlock.of("this.$moduleInstanceName = ${it.className.camelCase()}\n return this"))
                    .returns(factoryClassName.nestedClass("Builder"))

            builderSpec.addFunction(addModuleFuncSpec.build())
        }

        val stringBuilder = java.lang.StringBuilder()

        stringBuilder.append("return $CLASS_SIMPLE_NAME(")
        properties.forEach {
            stringBuilder.append("this.$it,")
        }

        stringBuilder.deleteCharAt(stringBuilder.lastIndex)
        stringBuilder.append(')')

        val createFuncSpec = FunSpec.builder("create")
                .addCode(stringBuilder.toString())
                .returns(factoryClassName)

        builderSpec.addFunction(createFuncSpec.build())


        //addModule func


        typeSpec.addType(
                builderSpec.build()
        )

    }

    private fun createFunctions(
            typeSpec: TypeSpec.Builder,
            modulePropertyNames: java.util.ArrayList<String>,
            classInfoList: List<ClassInfo>
    ) {
        //遍历 所有的类
        classInfoList.forEachIndexed { index, classInfo ->

            //把每个类的方法添加进去
            classInfo.methods.forEach {

                val funSpecBuilder = FunSpec.builder(it.name)

                //方法签名
                it.parameters.forEachIndexed { index, argTypeName ->
                    val argType = useKotlinType(argTypeName.asType().asTypeName())

                    funSpecBuilder.addParameter(
                            ParameterSpec.builder(
                                    argTypeName.simpleName.toString(),
                                    argType
                            ).build()
                    )
                }


                //代码块
                val codeFormatStringBuilder = StringBuilder()


                //val result = target.call(varargs)
                //return result
                val formatArgs = Array(it.parameters.size + 3) { "" }
                formatArgs[0] = "result"
                formatArgs[1] = modulePropertyNames[index]

                codeFormatStringBuilder.append("val %L = %L.${it.name}(")


                it.parameters.forEachIndexed { index, name ->
                    codeFormatStringBuilder.append("%L,")

                    //arg1  arg1 所有参数都按照 索引命名
                    formatArgs[index + 2] = name.simpleName.toString()
                }

                if (it.parameters.isNotEmpty())
                    codeFormatStringBuilder.deleteCharAt(codeFormatStringBuilder.lastIndex)
                codeFormatStringBuilder.append(")\n")

                codeFormatStringBuilder.append("return %L")

                formatArgs[formatArgs.size - 1] = "result"

                val codeBlock = CodeBlock.Builder()
                        .add(format = codeFormatStringBuilder.toString(), args = *formatArgs)
                        .build()

                funSpecBuilder.addCode(codeBlock)
                        .addAnnotation(JavascriptInterface::class.java)

                //返回值
                funSpecBuilder.returns(useKotlinType(it.returnTypeName))


                //add func

                typeSpec.addFunction(funSpecBuilder.build())

            }

        }

    }

    private fun useKotlinType(argTypeName: TypeName): TypeName {

        if (argTypeName == ClassName.bestGuess("java.lang.String"))
            return ClassName.bestGuess("kotlin.String")
        return argTypeName
    }

    private fun addProperties(
            typeSpec: TypeSpec.Builder,
            classInfoList: List<ClassInfo>
    ): ArrayList<String> {

        val moduleFieldNames = arrayListOf<String>()
        //每个module 的声明
        classInfoList.forEach {

            val moduleInstanceName = it.className.toLowerCase() + "Instance"

            moduleFieldNames.add(moduleInstanceName)

            //使用primaryConstructor
//            val propertySpec = PropertySpec.builder(
//                moduleInstanceName,
//                it.classTypeName,
//                KModifier.LATEINIT
//            ).mutable(true).build()


//            typeSpec.addProperty(propertySpec)
        }
        return moduleFieldNames
    }

    class ClassInfo(
            val pkgName: String,
            val className: String,
            val classTypeName: TypeName,
            val methods: List<MethodInfo>
    )

    /**
     * [name] method name
     * [returnTypeName] return type canonical name
     * [parameters] every parameter's canonical name
     */
    class MethodInfo(
            val name: String,
            val returnTypeName: TypeName,
            val parameters: List<VariableElement>
    )

}